package net.sureon.common.security;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * utility class, security related.
 * 
 * @author jason
 */
public class SecurityUtils {
	private static final Log logger = LogFactory.getLog(SecurityUtils.class);
	private static final String xkey = "AES";
	private static final String xMDType = "SHA1";
	private final static int BYTE_LENGTH_SHA1 = 20;
	public static final String SHA512_ALGORITHM = "SHA-512";
	private final static long THIRTY_DAYS = (long) 30 * 24 * 60 * 60 * 1000;

	public static boolean verifyMessageDigest(String salt, String ticket,
			String strMessage) {
		byte[] bytes = Base64.decodeBase64(ticket.getBytes());
		byte[] bytesTS = new byte[bytes.length - BYTE_LENGTH_SHA1];
		byte[] sha1MsgIn = new byte[BYTE_LENGTH_SHA1];
		System.arraycopy(bytes, 0, sha1MsgIn, 0, BYTE_LENGTH_SHA1);
		System.arraycopy(bytes, BYTE_LENGTH_SHA1, bytesTS, 0, bytesTS.length);
		String ts = null;
		try {
			ts = new String(bytesTS, "UTF-8");
			if (StringUtils.isNumeric(ts)) {
				long timestamp = Long.parseLong(ts);
				long currentTime = System.currentTimeMillis();
				long timeDiff = currentTime - timestamp;
				if (timeDiff > 0 && timeDiff < THIRTY_DAYS) {
					// in 10 minutes.
					byte[] sha1Msg = computeMessageDigest(getRawData(ts, salt,
							strMessage));
					if (MessageDigest.isEqual(sha1Msg, sha1MsgIn)) {
						return true;
					} else {
						logger
								.error("Token is not valid as invalid MessageDigest, ticket is: "
										+ ticket
										+ ". ("
										+ strMessage
										+ ","
										+ salt + ")");
					}
				} else {
					logger.error("Token is not valid as expired, ticket is: "
							+ ticket + ". Its gerated time is: " + timestamp
							+ ". (" + strMessage + "," + salt + ")");
				}
			} else {
				logger
						.error("Token is not valid as inlcuded the non-numeric TS part, ticket is: "
								+ ticket
								+ ". ("
								+ strMessage
								+ ","
								+ salt
								+ ")");
			}
		} catch (UnsupportedEncodingException e) {
			/* this shouldn't happen as we are using utf-8 */
			logger.error("Failed to new TS string from bytes in UTF-8", e);
		}
		return false;
	}

	private static byte[] getRawData(String useTime, String salt, String message)
			throws UnsupportedEncodingException {
		return (message + useTime + salt).getBytes("UTF-8");
	}

	/**
	 * Computes a message digest consisting of the byte representation of a time
	 * and a byte array.
	 * 
	 * @param byMessage
	 * @return
	 */
	private static byte[] computeMessageDigest(byte[] byMessage) {
		try {
			MessageDigest md = MessageDigest.getInstance(xMDType);
			md.update(byMessage);
			return md.digest();
		} catch (NoSuchAlgorithmException e) {
			/* shouldn't happen as we use a standard algorithm */
			logger.error("Failed to Compute Message Digest message:"
					+ new String(Base64.encodeBase64(byMessage)), e);
		}
		return null;
	}

	/**
	 * Ticket = Base64Encoding(SHA(message+TS+key) + TS)
	 * 
	 * @param salt
	 * @param message
	 * @return
	 */
	public static String makeMessageDigest(String salt, String message) {
		return makeMessageDigest(System.currentTimeMillis() + "", salt, message);
	}

	/**
	 * Ticket = Base64Encoding(SHA(message+TS+key) + TS)
	 * 
	 * @param ts
	 *            timestamp in String.
	 * @param salt
	 * @param message
	 * @return
	 */
	private static String makeMessageDigest(String ts, String salt,
			String message) {
		try {
			byte[] sha1Msg = computeMessageDigest(getRawData(ts, salt, message));
			if (sha1Msg != null) {
				byte[] merged = mergeByteArray(sha1Msg, ts.getBytes("UTF-8"));
				return new String(Base64.encodeBase64(merged));
			}
		} catch (UnsupportedEncodingException e) {
			/* this shouldn't happen as we are using utf-8 */
			logger.error("Failed to make message digest, useTime: " + ts
					+ "; key:" + salt + "; data:" + message, e);
		}
		return null;
	}

	private static byte[] mergeByteArray(byte[] source1, byte[] source2) {
		byte[] mergeArray = new byte[source1.length + source2.length];
		System.arraycopy(source1, 0, mergeArray, 0, source1.length);
		System
				.arraycopy(source2, 0, mergeArray, source1.length,
						source2.length);

		return mergeArray;
	}

	/**
	 * Construct and return a private key encoded as a hex string. Note that in
	 * our case, the key generation should occur at some point prior to calling
	 * encrypt. The key must have been distributed to the receiver before they
	 * can call decrypt.
	 * 
	 * @return key in hex format
	 * @throws NoSuchAlgorithmException
	 */
	public static String makeSecretKey() throws NoSuchAlgorithmException {
		String strKeyBytes = null;
		// step 1: generate a secret key
		KeyGenerator kg = null;
		kg = KeyGenerator.getInstance(xkey);
		kg.init(128);
		SecretKey key = kg.generateKey();

		// get the encoded value for the string and convert to hex
		byte[] encodedKey = key.getEncoded();
		strKeyBytes = toHexString(encodedKey);
		return strKeyBytes;
	}

	private static final String hexCharsStr = "0123456789abcdef";

	public static String toHexString(byte[] b) {
		StringBuilder str = new StringBuilder();
		for (int i = 0; i < b.length; i++) {
			str.append(hexCharsStr.charAt(b[i] >>> 4 & 0x0F));
			str.append(hexCharsStr.charAt(b[i] & 0x0F));
		}
		return str.toString();
	}

	/**
	 * Generate a random string, whose length will be between the minLength and
	 * maxLength values
	 * 
	 * @param minLength
	 *            minimum length of the string
	 * @param maxLength
	 *            maximum length of the string
	 * @return a random string. The string contains most printable ascii char.
	 *         Charater set is decimal 33 (char !) to decimal 126 (char ~) i.e.
	 *         all numbers 0-9, upper and lower case char a-z A-Z and
	 *         !@#$%^&*()-_=+\|[]{}~`'":;,./<>?
	 */
	private static char zero = '0';
	private static char z = 'z';
	public static String generateRandomString(int minLength, int maxLength) {
		int n = generateRandomNumber(minLength, maxLength);
		byte b[] = new byte[n];
		for (int i = 0; i < n; i++)
			b[i] = (byte) generateRandomNumber(zero, z);
		return new String(b);
	}

	private static Random rn = new Random();

	/**
	 * Actual random numbers are obtained using nextInt(), and then knocked down
	 * to the relevant range using the modulo ("%") operator.
	 * 
	 * @param lo
	 *            value of the random number
	 * @param hi
	 *            value of the random number
	 * @return a random number between the lo and hi value (inclusive).
	 */
	public static int generateRandomNumber(int lo, int hi) {
		int n = hi - lo + 1;
		int i = rn.nextInt() % n;
		if (i < 0)
			i = -i;
		return lo + i;
	}

	/**
	 * Generate the SHA-512 Hash of the given source string
	 * 
	 * @param sourceStr
	 *            source string
	 * @return The SHA-512 Hash value of the source string
	 * @throws NoSuchAlgorithmException
	 *             if the SHA-256 algorithm cannot be found.
	 */
	public static String generateSHA512Hash(String sourceStr) {
		try {
			return generateHash(sourceStr, SHA512_ALGORITHM);
		} catch (NoSuchAlgorithmException e) {
			logger.error("encrypt string failed.", e);
		}
		return null;
	}

	/**
	 * Generate the Hash of source string using the algorithm passed
	 * 
	 * @param sourceStr
	 *            source string
	 * @param algorithm
	 *            supported by MessageDigest. MD2: The MD2 message digest
	 *            algorithm as defined in RFC 1319. MD5: The MD5 message digest
	 *            algorithm as defined in RFC 1321. SHA-1: The Secure Hash
	 *            Algorithm, as defined in Secure Hash Standard, NIST FIPS
	 *            180-1. SHA-256, SHA-384, and SHA-512: New hash algorithms for
	 *            which the draft Federal Information Processing Standard 180-2,
	 *            Secure Hash Standard (SHS) is now available. SHA-256 is a
	 *            256-bit hash function intended to provide 128 bits of security
	 *            against collision attacks, while SHA-512 is a 512-bit hash
	 *            function intended to provide 256 bits of security. A 384-bit
	 *            hash may be obtained by truncating the SHA-512 output.
	 * @return Hash of the source string
	 * @throws NoSuchAlgorithmException
	 *             if the algorithm is cannot be found.
	 */
	private static String generateHash(String sourceStr, String algorithm)
			throws NoSuchAlgorithmException {
		StringBuilder sb;
		byte[] array;
		// set up md
		MessageDigest md = null;
		md = MessageDigest.getInstance(algorithm);
		// run through md
		md.update(sourceStr.getBytes());

		// convert digest to a hex string
		array = md.digest();
		sb = new StringBuilder();
		for (int j = 0; j < array.length; ++j) {
			int b = array[j] & 0xFF;
			if (b < 0x10) {
				sb.append('0');
			}
			sb.append(Integer.toHexString(b));
		}
		return sb.toString();
	}
}
